classdef dotsQueryableHIDKeyboard < dotsQueryableHID
    % @class dotsQueryableHIDKeyboard
    % Acquires and classifies data from a HID keyboard device.
    % @details
    % dotsQueryableHIDKeyboard extends the dotsQueryableHID superclass to
    % manage HID keyboard devices.  Many integrated and USB keybords
    % (nearly all) are HID keyboards.
    % @details
    % dotsQueryableHIDKeyboard is a usable class.  Use the newObject()
    % method of dotsTheQueryablesManager to create new
    % dotsQueryableHIDKeyboard objects.
    % @details
    % By default, dotsQueryableHIDKeyboard tracks value changes on its
    % keys.  So the queryNextTracked() method can help make sure
    % that no key-related phenomenons are missed.
    % @details
    % <b>A Note about Keys of Interest:</b>
    % @details
    % Since dotsQueryableHIDKeyboard is a subclass of dotsQueryable, it
    % uses a "data space" to represent the pressed/unpressed states of
    % its keys.  A standard keyboard with about 100 keys suggests a data
    % space of size 2^100, which is impractical.
    % @details
    % Instead of representing every single key, a dotsQueryableHIDKeyboard
    % object may represent a few "keys of interest".  These are specified
    % in the keysOfInterest property.  They can be changed arbitrarily. See
    % dotsTheMachineConfiguration() for machine-specific, default 
    % keysOfInterest.  See mexHIDUsage() for the name that identifies each
    % key.
    % @ingroup queryable
    
    properties
        % struct of HID parameters to identify the key elements of the
        % keyboard.
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        keyMatching;
        
        % Cell array of usage names identifying keyboard keys to be
        % represented.  Other keys are ignored.  See
        % dotsTheMachineConfiguration() for machine-specific, default
        % keysOfInterest.  See mexHIDUsage() for the name that identifies
        % each key.
        % @details
        % The object will not reflect changes to keysOfInterest until its
        % initialize() method is called.
        keysOfInterest;
        
        % array of element cookies as returned from
        % mexHID('findMatchingElements'), for the key elements that
        % mexHID() found on the keyboard.
        keyCookies;
    end
    
    methods
        % Constructor takes no arguments.
        % @details
        % User should use dotsTheQueryablesManager to create new objects.
        function self = dotsQueryableHIDKeyboard
            self = self@dotsQueryableHID;
        end

        % Locate a keyboard device and its key elements.
        % @details
        % Extends the dotsQueryableHID openMexHIDDevice() method to also
        % locate keys of interest on the keyboard.
        function isOpen = openMexHIDDevice(self)
            isOpen = self.openMexHIDDevice@dotsQueryableHID;
            
            self.keyCookies = [];
            if isOpen
                % find keys of interest
                matching = self.keyMatching;
                for ii = 1:length(self.keysOfInterest)
                    % match each key by Usage number
                    usageName = self.keysOfInterest{ii};
                    usageNum = mexHIDUsage.numberForUsageNameOnPage( ...
                        usageName, matching.UsagePage);
                    matching.Usage = usageNum;
                    
                    keyCookie = mexHID('findMatchingElements', ...
                        self.deviceID, matching);
                    if keyCookie > 0
                        self.keyCookies(end+1) = keyCookie;
                    end
                end
                
                self.queueCookies = self.keyCookies;
                isOpen = ~isempty(self.queueCookies);
            end
        end

        % Show a figure with continuously updated device data.
        % @details
        % Opens a new figure and plots axis and button data.  Continuously
        % updates the plot data as long as the figure is open.  Should be a
        % handy way to test or inspect a device.
        % @details
        % Shows whether each key of interest is pressed or released.
        function plotData(self)
            if self.isAvailable
                
                f = figure;
                
                keyDims = self.dimensionByID(self.keyCookies);
                n = length(keyDims);
                ticks = unique([ ...
                    self.dimensions(keyDims).minValue, ...
                    self.dimensions(keyDims).maxValue, ...
                    self.dimensions(keyDims).neutralValue]);
                ax = axes('Parent', f, ...
                    'YLim', [0, n+1], ...
                    'YTick', 1:n, ...
                    'YTickLabel', {self.dimensions(keyDims).name}, ...
                    'XLim', [min(ticks), max(ticks)+eps], ...
                    'XTick', ticks);
                ylabel(ax, 'keys of interest');
                keyLine = line(zeros(1,n), 1:n, 'Parent', ax, ...
                    'LineStyle', 'none', 'Marker', '*');
                
                while ishandle(f)
                    mexHID('step');
                    set(keyLine, 'XData', self.currentState(keyDims,2));
                    drawnow;
                    WaitSecs(.002);
                end
            end
        end
    end
    
    methods (Access = protected)
        % Configre the array of dotsFiniteDimension to represent the space
        % of keys of interest.
        function setupDimensions(self)
            nKeys = length(self.keyCookies);
            keyInfo = mexHID('summarizeElements', ...
                self.deviceID, self.keyCookies);
            for ii = 1:nKeys
                self.dimensions(ii) = dotsFiniteDimension.newLinear( ...
                    keyInfo(ii).CalibrationMin, ...
                    keyInfo(ii).CalibrationMax, ...
                    2);
                
                keyName = mexHIDUsage.nameForUsageNumberOnPage( ...
                    keyInfo(ii).Usage, keyInfo(ii).UsagePage);
                self.dimensions(ii).name = keyName;
                self.dimensions(ii).ID = self.keyCookies(ii);
                self.dimensions(ii).neutralValue = ...
                    self.dimensions(ii).minValue;
            end
            self.isTrackingDimension = true(1, nKeys);
        end

        % Identify handy, reusable regions in the object's data space.
        % @details
        %   - pressed and released for each key of interest
        %   .        
        function setupDefaultPhenomenons(self)
            temp = self.getPhenomenonTemplate;
            
            % pressed and released each, for each key
            keyDims = self.dimensionByID(self.keyCookies);
            for kk = keyDims
                d = self.dimensions(kk);
                pressed = dotsPhenomenon.partition(temp, ...
                    d.name, d.maxValue, '==');
                pressed.name = sprintf('pressed_%s', d.name);
                self.addPhenomenonToGroup(pressed, 'pressed');
                
                released = dotsPhenomenon.complement(pressed);
                released.name = sprintf('released_%s', d.name);
                self.addPhenomenonToGroup(released, 'released');
            end
        end
    end
end